// $Id: EnumerableBase.java 71 2012-11-26 02:07:40Z marcel.kloubert $

/*****************************************************************************
 *                                                                           *
 *   Licensed to the Apache Software Foundation (ASF) under one              *
 *   or more contributor license agreements.  See the NOTICE file            *
 *   distributed with this work for additional information                   *
 *   regarding copyright ownership.  The ASF licenses this file              *
 *   to you under the Apache License, Version 2.0 (the                       *
 *   "License"); you may not use this file except in compliance              *
 *   with the License.  You may obtain a copy of the License at              *
 *                                                                           *
 *       http://www.apache.org/licenses/LICENSE-2.0                          *
 *                                                                           *
 *   Unless required by applicable law or agreed to in writing,              *
 *   software distributed under the License is distributed on an             *
 *   "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY                  *
 *   KIND, either express or implied.  See the License for the               *
 *   specific language governing permissions and limitations                 *
 *   under the License.                                                      *
 *                                                                           *
 *****************************************************************************/


package net.metalvz.linq;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A basic sequence.
 * 
 * @author Marcel Joachim Kloubert <marcel.kloubert@gmx.net>
 *
 * @param <T> The type of the items.
 */
public abstract class EnumerableBase<T> implements IEnumerable<T> {
	/**
	 * Initializes a new instance of that class.
	 */
	protected EnumerableBase() {
		
	}
	
	@Override
	public boolean all(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		boolean result = true;
		for (T item: this) {
			if (predicate.invoke(item) == false) {
				result = false;
				break;
			}
		}
		
		return result;
	}
	
	@Override
	public boolean any() {
		return this.any(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}
		});
	}

	@Override
	public boolean any(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		boolean result = false;
		for (T item: this) {
			if (predicate.invoke(item)) {
				result = true;
				break;
			}
		}
		
		return result;
	}
	
	@Override
	public <U> IEnumerable<U> cast() {
		return this.select(new Func1<T, U>() {
			@SuppressWarnings("unchecked")
			public U invoke(T item) {				
				return (U)item;
			}
		});
	}
	
	@Override
	public boolean contains(T itemToCheck) {
		return this.indexOf(itemToCheck) > -1;
	}
	
	@Override
	public int count() {
		int result = 0;
		for (@SuppressWarnings("unused") T item: this) {
			++result;
		}
		
		return result;
	}
	
	@Override
	public T first() {
		return this.first(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}			
		});
	}
	
	@Override
	public T first(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		boolean found = false;
		
		T result = null;
		for (T item: this) {
			if (predicate.invoke(item)) {
				result = item;
				found = true;
				
				break;
			}
		}
		
		if (found == false) {
			throw new EnumerableException("Sequence does NOT contain elements!", this);
		}
		
		return result;
	}
	
	@Override
	public T firstOrDefault() {
		return this.firstOrDefault(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}			
		});
	}
	
	@Override
	public T firstOrDefault(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		T result = null;
		for (T item: this) {
			if (predicate.invoke(item)) {
				result = item;
				break;
			}
		}
		
		return result;
	}
	
	@Override
	public <K> IEnumerable<IGrouping<K, T>> groupBy(Func1<T, K> keySelector) {
		if (keySelector == null) {
			throw new NullPointerException("keySelector");
		}
		
		HashMap<K, ArrayList<T>> temp = new HashMap<K, ArrayList<T>>();
		for (T item: this) {
			K key = keySelector.invoke(item);
			
			if (temp.containsKey(key) == false) {
				temp.put(key, new ArrayList<T>());
			}
			
			temp.get(key)
			    .add(item);
		}
		
		ArrayList<IGrouping<K, T>> groupings = new ArrayList<IGrouping<K,T>>();
		for (K key: temp.keySet()) {
			ArrayList<T> list = temp.get(key);
			
			groupings.add(new EnumerableGrouping<K, T>(key,
					                                   Enumerable.asEnumerable(list)));
		}
		
		return Enumerable.asEnumerable(groupings);
	}
	
	@Override
	public int indexOf(T itemToSearch) {
		int result = -1;
		
		int currentIndex = 0;
		for (T item: this) {
			boolean found = false;
			if (item == null) {
				found = itemToSearch == null;
			}
			else {
				found = item.equals(itemToSearch);
			}
			
			if (found) {
				result = currentIndex;
				break;
			}
			
			++currentIndex;
		}
		
		return result;
	}
	
	@Override
	public T last() {
		return this.last(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}			
		});
	}
	
	@Override
	public T last(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		boolean found = false;
		
		T result = null;
		for (T item: this) {
			if (predicate.invoke(item)) {
				result = item;
				found = true;
			}
		}
		
		if (found == false) {
			throw new EnumerableException("Sequence does NOT contain elements!", this);
		}
		
		return result;
	}
	
	@Override
	public T lastOrDefault() {
		return this.lastOrDefault(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}			
		});
	}
	
	@Override
	public T lastOrDefault(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		T result = null;
		for (T item: this) {
			if (predicate.invoke(item)) {
				result = item;
			}
		}
		
		return result;
	}
	
	@Override
	public IEnumerable<T> orderBy(Comparator<T> comparator) {
		if (comparator == null) {
			throw new NullPointerException("comparator");
		}
		
		List<T> listToSort = this.toList();
		Collections.sort(listToSort, comparator);
		
		return Enumerable.asEnumerable(listToSort);
	}
	
	@Override
	public IEnumerable<T> orderByDescending(Comparator<T> comparator) {
		if (comparator == null) {
			throw new NullPointerException("comparator");
		}
		
		return this.orderBy(Collections.reverseOrder(comparator));
	}
	
	@Override
	public <U> IEnumerable<U> select(Func1<T, U> selector) {
		if (selector == null) {
			throw new NullPointerException("selector");
		}
		
		ArrayList<U> result = new ArrayList<U>();
		for (T item: this) {
			result.add(selector.invoke(item));
		}

		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public <U> IEnumerable<U> selectMany(Func1<T, Iterable<U>> selector) {
		if (selector == null) {
			throw new NullPointerException("selector");
		}
		
		ArrayList<U> result = new ArrayList<U>();
		for (T item: this) {
			for (U subItem: selector.invoke(item)) {
				result.add(subItem);
			}
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public <U> IEnumerable<U> selectManyArray(Func1<T, U[]> selector) {
		if (selector == null) {
			throw new NullPointerException("selector");
		}
		
		ArrayList<U> result = new ArrayList<U>();
		for (T item: this) {
			for (U subItem: selector.invoke(item)) {
				result.add(subItem);
			}
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public T single() {
		return this.single(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}
		});
	}

	@Override
	public T single(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		ArrayList<T> temp = new ArrayList<T>();
		for (T item: this) {
			if (predicate.invoke(item)) {
				if (temp.size() > 0) {
					throw new EnumerableException("The sequence contains more than ONE element!", this);
				}
				else {
					temp.add(item);
				}
			}
		}
		
		if (temp.size() < 1) {
			throw new EnumerableException("The sequence contains NO element!", this);
		}
		
		return temp.get(0);
	}

	@Override
	public T singleOrDefault() {
		return this.singleOrDefault(new Predicate<T>() {
			public boolean invoke(T item) {
				return true;
			}
		});
	}

	@Override
	public T singleOrDefault(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		ArrayList<T> temp = new ArrayList<T>();
		for (T item: this) {
			if (predicate.invoke(item)) {
				if (temp.size() > 0) {
					throw new EnumerableException("The sequence contains more than ONE element!", this);
				}
				else {
					temp.add(item);
				}
			}
		}
		
		return temp.size() > 0 ? temp.get(0) : null;
	}
	
	@Override
	public T[] toArray(T[] dummyArray) {
		if (dummyArray == null) {
			throw new NullPointerException("dummyArray");
		}
		
		return this.toList().toArray(dummyArray);
	}
	
	@Override
	public IEnumerable<T> skip(int count) {
		ArrayList<T> result = new ArrayList<T>();
		
		// skip these
		Iterator<T> i = this.iterator();
		while (count > 0 && i.hasNext()) {
			i.next();
			--count;
		}
		
		// now take the rest
		while (i.hasNext()) {
			result.add(i.next());
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public IEnumerable<T> skipWhile(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		ArrayList<T> result = new ArrayList<T>();
		
		// skip these
		Iterator<T> i = this.iterator();
		while (i.hasNext()) {
			T item = i.next();
			if (predicate.invoke(item) == false) {
				result.add(item);
				break;
			}
		}
		
		// now take the rest
		while (i.hasNext()) {
			result.add(i.next());
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public IEnumerable<T> take(int count) {
		ArrayList<T> result = new ArrayList<T>();
		
		Iterator<T> i = this.iterator();
		while (count > 0 && i.hasNext()) {
			result.add(i.next());
			--count;
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public IEnumerable<T> takeWhile(Predicate<T> predicate) {
		if (predicate == null) {
			throw new NullPointerException("predicate");
		}
		
		ArrayList<T> result = new ArrayList<T>();
		
		Iterator<T> i = this.iterator();
		while (i.hasNext()) {
			T item = i.next();
			if (predicate.invoke(item)) {
				result.add(item);	
			}
			else {
				break;
			}
		}
		
		return Enumerable.asEnumerable(result);
	}
	
	@Override
	public List<T> toList() {
		ArrayList<T> result = new ArrayList<T>();
		for (T item: this) {
			result.add(item);
		}
		
		return result;
	}	
	
	@Override
	public <K> Map<K, T> toMap(Func1<T, K> keySelector) {
		if (keySelector == null) {
			throw new NullPointerException("keySelector");
		}
		
		HashMap<K, T> result = new HashMap<K, T>();
		for (T item: this) {
			K key = keySelector.invoke(item);
			if (result.containsKey(key)) {
				throw new EnumerableException("Duplicated key was produced for two elements!", this);
			}
			
			result.put(key, item);
		}
		
		return result;
	}
		
	@Override
	public IEnumerable<T> where(Predicate<T> filter) {
		if (filter == null) {
			throw new NullPointerException("filter");
		}
		
		ArrayList<T> result = new ArrayList<T>();
		for (T item: this) {
			if (filter.invoke(item)) {
				result.add(item);
			}
		}

		return Enumerable.asEnumerable(result);
	}	
}
